﻿//
// © Algobox Ltd - Benn Eichhorn
//
using System;
using System.Collections.Generic;
using System.Text;

namespace Algobox.Toolbox.Collections.TimeSeries
{
    public class OHLCMinuteBars
    {
        #region Constructors


        public OHLCMinuteBars(DateTime startTime, TimeSpan barSize, int numBars)
        {
            _startTime = startTime;
            _barSize = barSize.TotalMinutes;
            _arraySize = numBars;

            _endTime = startTime + TimeSpan.FromMinutes(barSize.TotalMinutes * numBars);

            _highs = new double[_arraySize];
            _lows = new double[_arraySize];
            _mids = new double[_arraySize];
            _opens = new double[_arraySize];
            _closes = new double[_arraySize];
            _volumes = new ulong[_arraySize];
            _barStarts = new double[_arraySize];
            _barEnds = new double[_arraySize];
            _hasData = new bool[_arraySize];
        }

        public OHLCMinuteBars(DateTime startTime, DateTime endTime, TimeSpan barSize)
        {
            if (startTime >= endTime)
                throw new InvalidOperationException("Invalid range, start time must be earlier than end time");

            TimeSpan length = endTime - startTime;

            if (length < barSize)
            {
                length = barSize;
                endTime = startTime + barSize;
            }

            _startTime = startTime;
            _endTime = endTime;
            _barSize = barSize.TotalMinutes;
            _arraySize = (int)(length.TotalMinutes / barSize.TotalMinutes) + 1;

            _highs = new double[_arraySize];
            _lows = new double[_arraySize];
            _mids = new double[_arraySize];
            _opens = new double[_arraySize];
            _closes = new double[_arraySize];
            _volumes = new ulong[_arraySize];
            _barStarts = new double[_arraySize];
            _barEnds = new double[_arraySize];
            _hasData = new bool[_arraySize];
        }

        #endregion
//////////////////
        #region Member Variables

        DateTime _startTime;
        DateTime _endTime;
        double _barSize;

        int _arraySize;

        double[] _highs;
        double[] _lows;
        double[] _mids;
        double[] _opens;
        double[] _closes;
        ulong[] _volumes;

        double[] _barStarts;
        double[] _barEnds;

        bool[] _hasData;

        int _maxIndex;

        #endregion
//////////////////
        #region Properties

        public double High { get { return _highs[_maxIndex]; } }
        public double Low { get { return _lows[_maxIndex]; } }
        public double Open { get { return _opens[_maxIndex]; } }
        public double Close { get { return _closes[_maxIndex]; } }
        public double Mid { get { return _mids[_maxIndex]; } }
        public double Volume { get { return _volumes[_maxIndex]; } }
        public double HighLowRange { get { return _highs[_maxIndex] - _lows[_maxIndex]; } }

        public DateTime StartTime { get { return _startTime; } }
        public DateTime EndTime { get { return _endTime; } }

        /// <summary>
        /// Number of minutes that make up each bar
        /// </summary>
        public double BarSize { get { return _barSize; } }

        /// <summary>
        /// Gets the current bar index
        /// </summary>
        public int BarIndex { get { return _maxIndex; } }

        #endregion
//////////////////
        #region Functions

        /// <summary>
        /// Try to add an OHLC bar to the timeseries
        /// </summary>
        /// <param name="ohlc">A OHLC bar with prices to be added to the timeseries bar that corresponds to the OHLC TimeStamp</param>
        /// <returns>Return true when the OHLC bar was added, false when the OHLC bar TimeStamp is outside the range of this timeseries</returns>
        public bool TryAdd(Algobox.Objects.Finance.Prices.OHLC ohlc)
        {
            if (ohlc.TimeStamp < _startTime || ohlc.TimeStamp > _endTime)
                return false;

            double time = (ohlc.TimeStamp - _startTime).TotalMinutes;
            int index = (int)(time / _barSize);

            _hasData[index] = true;

            if(index > _maxIndex + 1)
                CheckBackFill(index, ohlc.Open);
            
            if (_barStarts[index] == 0)
            {
                _barStarts[index] = time;
                _barEnds[index] = time;
                _opens[index] = ohlc.Open;
                _closes[index] = ohlc.Close;
                _highs[index] = ohlc.High;
                _lows[index] = ohlc.Low;
                _mids[index] = (ohlc.High + ohlc.Low) / 2;
                _volumes[index] = ohlc.Volume;
            }
            else
            {
                if (time <= _barStarts[index])
                {
                    _barStarts[index] = time;
                    _opens[index] = ohlc.Open;
                }
                if (time >= _barEnds[index])
                {
                    _barEnds[index] = time;
                    _closes[index] = ohlc.Close;
                }
                if (ohlc.High > _highs[index])
                {
                    _highs[index] = ohlc.High;
                    _mids[index] = (ohlc.High + _lows[index]) / 2;
                }
                if (ohlc.Low < _lows[index])
                {
                    _lows[index] = ohlc.Low;
                    _mids[index] = (_highs[index] + ohlc.Low) / 2;
                }
                _volumes[index] += ohlc.Volume;
            }

            if (index < _maxIndex - 1)
                CheckForwardFill(index);

            if (index > _maxIndex)
                _maxIndex = index;

            return true;
        }

        /// <summary>
        /// Try to add a price and/or volume to specific time in the timeseries
        /// </summary>
        /// <param name="timeStamp">The time stamp of the price to be added</param>
        /// <param name="price">The price to be added at this time in the timeseries</param>
        /// <param name="volume">The volume to be added at this time to the timeseries</param>
        /// <returns>Returns the 0-indexed bar number that this price has been entered into</returns>
        public bool TryAdd(DateTime timeStamp, double price, ulong volume, out int index)
        {
            if (timeStamp < _startTime || timeStamp > _endTime)
            {
                index = 0;
                return false;
            }

            double time = (timeStamp - _startTime).TotalMinutes;

            index = (int)(time / _barSize);

            _hasData[index] = true;

            if (index > _maxIndex && !_hasData[index - 1])
                CheckBackFill(index, price);
            
            if (_barStarts[index] == 0)
            {
                _barStarts[index] = time;
                _barEnds[index] = time;
                _opens[index] = price;
                _closes[index] = price;
                _highs[index] = price;
                _lows[index] = price;
                _mids[index] = price;
                _volumes[index] = volume;
            }
            else
            {
                if (price != 0 && time <= _barStarts[index])
                {
                    _barStarts[index] = time;
                    _opens[index] = price;
                }
                if (price != 0 && time >= _barEnds[index])
                {
                    _barEnds[index] = time;
                    _closes[index] = price;
                }
                if (price > _highs[index])
                {
                    _highs[index] = price;
                    _mids[index] = (price + _lows[index]) / 2;
                }
                if (price != 0 && (_lows[index] == 0 || price < _lows[index]))
                {
                    _lows[index] = price;
                    _mids[index] = (_highs[index] + price) / 2;
                }
                _volumes[index] += volume;
            }

            if (index < _maxIndex - 1)
                CheckForwardFill(index);

            if (index > _maxIndex)
                _maxIndex = index;

            return true;
        }

        public bool TryGetPreviousHighLow(int startIndex, int lookbackPeriod, out double high, out double low)
        {
            high = _highs[startIndex];
            low = _lows[startIndex];
            
            if (startIndex + 1 < lookbackPeriod)
                return false;

            while (--startIndex >= 0 && --lookbackPeriod > 0)
            {
                high = System.Math.Max(high, _highs[startIndex]);
                low = System.Math.Min(low, _lows[startIndex]);
            }
            return true;
        }


        public bool TryGetPreviousHigh(int startIndex, int lookbackPeriod, out double high)
        {
            high = _highs[startIndex];

            if (!_hasData[startIndex])
                return false;

            if (startIndex + 1 < lookbackPeriod)
                return false;

            while (--startIndex >= 0 && --lookbackPeriod > 0)            
                high = System.Math.Max(high, _highs[startIndex]);
            
            return true;
        }
        
        public bool TryGetPreviousLow(int startIndex, int lookbackPeriod, out double low)
        {
            low = _lows[startIndex];

            if (!_hasData[startIndex])
                return false;

            if (startIndex + 1 < lookbackPeriod)
                return false;

            while (--startIndex >= 0 && --lookbackPeriod > 0)
                low = System.Math.Min(low, _lows[startIndex]);
            
            return true;
        }

        /// <summary>
        /// Back fill the arrays if there is no data up until a specific index
        /// </summary>
        /// <param name="index">The index that will have data to look back from</param>
        /// <param name="openPrice">The opening price of the specific index that will be used as back fill price if not data is found</param>
        private void CheckBackFill(int index, double openPrice)
        {
            if(index == 0)
                return;

            int lastIndex = index - 1;
            bool foundData = false;
            for (; lastIndex > 0; lastIndex--)
            {
                if (_hasData[lastIndex])
                {
                    foundData = true;
                    break;
                }                    
            }

            double open, high, low, close, mid;

            if (foundData)
            {
                open = _opens[lastIndex];
                close = _closes[lastIndex];
                high = _highs[lastIndex];
                low = _lows[lastIndex];
                mid = _mids[lastIndex];

                // back fill
                for (int x = lastIndex + 1; x < index; x++)
                {
                    _opens[x] = open;
                    _closes[x] = close;
                    _highs[x] = high;
                    _lows[x] = low;
                    _mids[x] = mid;
                }
            }
            else
            {
                // back fill
                for (int x = 0; x < index; x++)
                {
                    _opens[x] = openPrice;
                    _closes[x] = openPrice;
                    _highs[x] = openPrice;
                    _lows[x] = openPrice;
                    _mids[x] = openPrice;
                }
            }
        }
        
        private void CheckForwardFill(int index)
        {
            double open = _opens[index];
            double close = _closes[index];
            double high = _highs[index];
            double low = _lows[index];

            while (!_hasData[++index])
            {
                _opens[index] = open;
                _closes[index] = close;
                _highs[index] = high;
                _lows[index] = low;
            }
        }

        public bool HasData(int barNumber)
        {
            if (barNumber < 0 || barNumber > _arraySize)
                return false;

            return _hasData[barNumber];
        }

        public double GetMovingAverage(int barNumber, int period, Data data = Data.Close, MAType maType = MAType.SMA)
        {
            if(barNumber > _arraySize)
                throw new ArgumentOutOfRangeException("barNumber", period, "barNumber must be less than the array size");

            if (barNumber < 0)
                throw new ArgumentOutOfRangeException("barNumber", period, "barNumber must be greater than 0");

            int endIdx;

            if (period > barNumber + 1)
            {
                endIdx = barNumber;
                period = barNumber;
            }
            else
            {
                endIdx = period - 1;
            }

            double[] array;
            switch (data)
            {
                case Data.Open: array = _opens; break;
                case Data.High: array = _highs; break;
                case Data.Low: array = _lows; break;
                case Data.Mid: array = _mids; break;
                default: array = _closes; break;
            }
            
            int begIndex;
            int numElements;
            double[] results = new double[barNumber + 1];
            TicTacTec.TA.Library.Core.RetCode ret = TicTacTec.TA.Library.Core.MovingAverage(0, barNumber, array, period, (TicTacTec.TA.Library.Core.MAType)(int)maType, out begIndex, out numElements, results);

            if (ret != TicTacTec.TA.Library.Core.RetCode.Success)
                throw new IndexOutOfRangeException("MovingAverage returned " + ret);
            
            return results[barNumber - begIndex];             
        }



        public double GetRSI(int barNumber, int period, Data data = Data.Close)
        {
            if(barNumber > _arraySize)
                throw new ArgumentOutOfRangeException("barNumber", period, "barNumber must be less than the array size");

            if (barNumber < 0)
                throw new ArgumentOutOfRangeException("barNumber", period, "barNumber must be greater than 0");

            int endIdx;

            if (period > barNumber + 1)
            {
                endIdx = barNumber;
                period = barNumber;
            }
            else
            {
                endIdx = period - 1;
            }

            double[] array;
            switch (data)
            {
                case Data.Open: array = _opens; break;
                case Data.High: array = _highs; break;
                case Data.Low: array = _lows; break;
                case Data.Mid: array = _mids; break;
                default: array = _closes; break;
            }

            int begIndex;
            int numElements;
            double[] results = new double[barNumber];
            TicTacTec.TA.Library.Core.RetCode ret = TicTacTec.TA.Library.Core.Rsi(0, barNumber, array, period, out begIndex, out numElements, results);

            if (ret != TicTacTec.TA.Library.Core.RetCode.Success)
                throw new IndexOutOfRangeException("MovingAverage returned " + ret);

            return results[barNumber - begIndex];  
        }

        public void GetBollingerBands(int barNumber, int period, double stdDev, out double top, out double middle, out double bottom, Data data = Data.Close, MAType maType = MAType.WMA)
        {
            if (barNumber > _arraySize)
                throw new ArgumentOutOfRangeException("barNumber", period, "barNumber must be less than the array size");

            if (barNumber < 0)
                throw new ArgumentOutOfRangeException("barNumber", period, "barNumber must be greater than 0");

            int endIdx;

            if (period > barNumber + 1)
            {
                endIdx = barNumber;
                period = barNumber;
            }
            else
            {
                endIdx = period - 1;
            }

            double[] array;
            switch (data)
            {
                case Data.Open: array = _opens; break;
                case Data.High: array = _highs; break;
                case Data.Low: array = _lows; break;
                case Data.Mid: array = _mids; break;
                default: array = _closes; break;
            }

            int begIndex;
            int numElements;
            double[] resultsTop = new double[barNumber];
            double[] resultsMiddle = new double[barNumber];
            double[] resultsBottom = new double[barNumber];
            
            TicTacTec.TA.Library.Core.RetCode ret = TicTacTec.TA.Library.Core.Bbands(0, barNumber, array, period, stdDev, stdDev, (TicTacTec.TA.Library.Core.MAType)(int)maType, out begIndex, out numElements, resultsTop, resultsMiddle, resultsBottom);
            
            if (ret != TicTacTec.TA.Library.Core.RetCode.Success)
                throw new IndexOutOfRangeException("MovingAverage returned " + ret);

            top = resultsTop[barNumber - begIndex];
            middle = resultsMiddle[barNumber - begIndex];
            bottom = resultsBottom[barNumber - begIndex];
        }

        #endregion
//////////////////
        #region Enums

        public enum Data
        {
            Open,
            High, 
            Low,
            Close,
            Mid
        }

        public enum MAType
        {
            SMA = 0,
            EMA = 1,
            WMA = 2,
            DEMA = 3,
            TEMA = 4,
            TRIMA = 5,
            KAMA = 6,
            MAMA = 7,
            T3 = 8,
        }

        #endregion
    }
}
